博客中代码地址:https://github.com/farliu/farpc.git
本章讲解自适应扩展机制,单独将这一块拿出来,是因为这段代码逻辑复杂,处理分支较多。如果不是从上一章看过来的,建议先看看上一章讲的IOC部分。基础不牢地动山摇的情况下无法分析。
自适应扩展机制解决了一个什么问题呢?下面取自dubbo官方的一段话:
有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。
这句话意思表达还是很明显,就是在方法被调用的才去选择调用哪个扩展点。但是这时问题来了,“选择”这个动作怎么来做?谁来做?对于这个两个问题,怎么做?是自适应扩展机制的核心。而谁来做?是自适应扩展机制的解决方案。弄清楚这两点,基本差不多了。我们先看一个demo。
自适应扩展机制示例 取自dubbo的单测(dubbo-common模块)。
org.apache.dubbo.common.extension.ext1.SimpleExt
1 2 3 4 impl1 =org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1impl2 =org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2 impl3 =org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3
ExtensionLoader_Adaptive_Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception { { SimpleExt ext = ExtensionLoader . getExtensionLoader(SimpleExt.class ) .getAdaptiveExtension() ; Map<String, String> map = new HashMap<String, String>() ; URL url = new URL("p1" , "1.2.3.4" , 1010, "path1" , map ) ; String echo = ext.echo(url, "haha" ); assert Equals("Ext1Impl1-echo" , echo ) ; } { SimpleExt ext = ExtensionLoader . getExtensionLoader(SimpleExt.class ) .getAdaptiveExtension() ; Map<String, String> map = new HashMap<String, String>() ; map.put("simple.ext" , "impl2" ); URL url = new URL("p1" , "1.2.3.4" , 1010, "path1" , map ) ; String echo = ext.echo(url, "haha" ); assert Equals("Ext1Impl2-echo" , echo ) ; } }
test_getAdaptiveExtension_defaultAdaptiveKey中的两个代码块唯一的区别就是,第二个代码块传入的map中保存了simple.ext->impl2的键值对,就拿到了SimpleExtImpl2的对象。这也正是自适应扩展机制解决的问题。
原理概括 为了更好的理解,先把原理交个底。我总觉得一步步验证比一步步发掘要更能理解一件事物。至于自适应机制的原理,dubbo会给需要自适应的方法生成一个代理类,通过javassist或jdk编译这段代码,得到Class。而代理类里面的逻辑,就是根据传入的Url对象中的变量取得扩展对象并调用。
SimpleExt接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 @SPI ("impl1" ) public interface SimpleExt { @Adaptive String echo (URL url, String s) ; @Adaptive({"key1" , "key2" }) String yell (URL url, String s) ; String bang (URL url, int i) ; }
dubbo为其生成自适应代理类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.apache.dubbo.common.extension.ext1; import org.apache.dubbo.common.extension.ExtensionLoader; public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt { public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) { throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!" ) ; } public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null" ) ; org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("key1" , url .getParameter ("key2" , "impl1" ) ); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url .toString () + ") use keys([key1, key2])" ); org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader . getExtensionLoader(org .apache .dubbo .common .extension .ext1 .SimpleExt.class ) .getExtension(extName ) ; return extension.yell(arg0, arg1); } public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null" ) ; org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("simple.ext" , "impl1" ) ; if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url .toString () + ") use keys([simple.ext])" ); org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader . getExtensionLoader(org .apache .dubbo .common .extension .ext1 .SimpleExt.class ) .getExtension(extName ) ; return extension.echo(arg0, arg1); } }
这就是dubbo自适应扩展的原理,看到这里是不是觉得自适应扩展也就那么回事。原本看起来如此神奇的功能,原理竟然如此简单。果然早在一百年前,周树人同志就告诫我们:“悲剧,是把美好的东西撕碎了给人看。”
话说回来,这里还有一点值得说。串通整个过程的@Adaptive注解,用起来有讲究。这一点很多博客都直接复制官网内容,一带而过。我想说的是,该注解确实可以标注在类和方法上,标注在方法上,用于自适应扩展机制,也就是本章的重点。而标注在类上,约定这个自适应扩展机制由程序员手动实现,不用dubbo生成扩展类。这里值得注意,dubbo为了这种扩展方式在很多地方都做了兼容。比如:
加载配置文件时,loadClass()中对标注了@Adaptive的类做缓存
createAdaptiveExtension()中为标注了@Adaptive的类再做了一次注入
getAdaptiveExtensionClass()在执行loadClass()还不存在已缓存的自适应扩展,也就是不存在标注了@Adaptive的类,才会创建。
还有需要注意一点,可以看到,上述所说的原理完全依赖于入参中是否存在URL,那么当入参中不存在URL对象,dubbo会怎么处理呢?直接抛异常?还是有妥善处理方式?
源码验证 我们以getAdaptiveExtension()为入口,该方法中常规的DCL校验缓存,然后调用createAdaptiveExtension()方法。
1 2 3 4 5 6 7 8 9 10 11 12 private T createAdaptiveExtension() { try { getExtensionClasses() ; if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } cachedAdaptiveClass = createAdaptiveExtensionClass() ; return injectExtension((T) cachedAdaptiveClass.new Instance() ); } catch (Exception e) { throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e .getMessage () , e); } }
createAdaptiveExtension()是整个自适应扩展机制的全景。主要包含了三个逻辑:
调用getExtensionClasses()获取所有扩展实现类
createAdaptiveExtensionClass()自动生成自适应实现类
injectExtension()为标注@Adaptive的类再做了一次注入
加载自适应代理类 getExtensionClasses()在上一章详细讲了,该方法用来加载配置文件中所有的扩展实现类。而需要再次提一下的是,在它调用的loadClass()中,对标注@Adaptive的类进行单独缓存,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void loadClass(Map <String , Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { ... if (clazz.isAnnotationPresent(Adaptive.class )) { if (cachedAdaptiveClass == null ) { cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("More than 1 adaptive class found: " + cachedAdaptiveClass.getName() + ", " + clazz.getName()); } } ... }
getExtensionClasses()在读取配置文件后,一个个加载其中的实现类,会检查该类是否标注@Adaptive,如果标注了则会将其保存在cachedAdaptiveClass变量中。这里也就是加载程序员人工编写自适应扩展类,这里有一个要求,一个接口只允许存在一个自适应扩展类。否则,抛异常。
自动生成自适应实现类 经过上述加载后,如果不存在人工编写的自适应扩展类,也还没自己创建自适应扩展类,那么开始由dubbo生成。
1 2 3 4 5 6 7 8 9 10 private Class<?> createAdaptiveExtensionClass() { String code = new AdaptiveClassCodeGenerator(type , cachedDefaultName ) .generate() ; ClassLoader classLoader = findClassLoader() ; org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader . getExtensionLoader(org .apache .dubbo .common .compiler .Compiler.class ) .getAdaptiveExtension() ; return compiler.compile(code, classLoader); }
createAdaptiveExtensionClass()总得来说就是完成两件事,一是生成自适应扩展类的代码,二是编译,生成class。于第二点,不做详细解释,我们主要查看代码生成规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public String generate() { if (!hasAdaptiveMethod() ) { throw new IllegalStateException("No adaptive method exist on extension " + type .getName () + ", refuse to create the adaptive class!" ); } StringBuilder code = new StringBuilder() ; code.append(generatePackageInfo() ); code.append(generateImports() ); code.append(generateClassDeclaration() ); Method[] methods = type .getMethods() ; for (Method method : methods) { code.append(generateMethod(method ) ); } code.append("}" ); if (logger.isDebugEnabled() ) { logger.debug(code.to String() ); } return code.to String() ; }
generate()中生成包、import、类定义,这三块代码实现都是通过String.format(),逻辑简单,这里一笔带过。generateMethod()逻辑分为生成方法主体、生成方法参数、生成方法定义异常,然后拼接在一起。而生成方法主体的逻辑分支较多,我先捋出一条思路,再一点点看。
分别处理是否有@Adaptive修饰的方法
定位URL对象的值
获取该接口自适应路由的key,用户获取URL中实现类的名字,并非空判断
通过实现类的名字,调用普通的SPI,生成扩展对象
调用目标方法
检测@Adaptive修饰 对于没有Adaptive修饰的方法,以SimpleExt.bang()方法为例。dubbo则不会为该方法生成具体逻辑,而是直接抛出异常,生成逻辑如下:
1 2 3 4 5 6 7 8 9 private static final String CODE_UNSUPPORTED = "throw new UnsupportedOperationException(\"The method %s of interface %s is not adaptive method!\");\n" ;private String generateMethodContent(Method method ) { Adaptive adaptiveAnnotation = method .getAnnotation ( Adaptive.class ); if (adaptiveAnnotation == null ) { return String.format(CODE_UNSUPPORTED, method , type .getName ( )); } ... }
定位URL对象的值 上文说到,自适应扩展机制,完全依赖URL对象,当不存在URL对象时,无法实现自适应扩展。而不是所有方法都需要URL做为入参的,那么dubbo是怎么处理的呢?
对于入参中存在URL对象,获取方式就是直接遍历获得。
对不入参不存在URL对象的方法,dubbo会遍历入参,通过反射调用入参中是否存在以get开头、返回值为URL的方法,并调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private static final String CODE_URL_NULL_CHECK = "if (arg%d == null) throw new IllegalArgumentException(\"url == null\");\n%s url = arg%d;\n" ;private String generateMethodContent(Method method ) { Adaptive adaptiveAnnotation = method .getAnnotation(Adaptive.class ) ; StringBuilder code = new StringBuilder(512) ; if (adaptiveAnnotation == null) { ... } else { int urlTypeIndex = getUrlTypeIndex(method ) ; if (urlTypeIndex != -1 ) { code.append(String . format(CODE_URL_NULL_CHECK, urlTypeIndex, URL .class .getName() , index)); } else { Class<?>[] pts = method .getParameterTypes() ; for (int i = 0 ; i < pts.length; ++i) { for (Method m : pts[i ] .getMethods() ) { String name = m.getName() ; if ((name.startsWith("get" ) || name.length() > 3 ) && Modifier . isPublic(m .getModifiers () ) && !Modifier . isStatic(m .getModifiers () ) && m.getParameterTypes() .length == 0 && m.getReturnType() == URL .class ) { return generateGetUrlNullCheck(i , pts [i ], name ) ; } } } throw new IllegalStateException("Failed to create adaptive class for interface " + type .getName () + ": not found url parameter or url attribute in parameters of method " + method .getName() ); } } ... }
获取URL中实现类的名字 获取实现类的名字,需要先获取URL对象中key。获取这个key,dubbo会先从Adaptive注解中取得,倘若注解中没有设置该值,则根据类名生成一个简单的名字当做key。比如SimpleExt,处理后会生成simple.ext当做key。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private String generateMethodContent(Method method ) { Adaptive adaptiveAnnotation = method .getAnnotation(Adaptive.class ) ; StringBuilder code = new StringBuilder(512) ; if (adaptiveAnnotation == null) { ... } else { ... String[] value = adaptiveAnnotation.value() ; if (value.length == 0 ) { String splitName = StringUtils . camelToSplitName(type .getSimpleName () , "." ); value = new String[] {splitName}; } return value; ... } }
接下来,是由上述生成key值,调用generateExtNameAssignment()从URL取得扩展名字。这段代码根据各类情况,分别处理,if判断很长,需要慢慢捋清楚。最终会生成一下代码
1 2 3 4 5 6 7 String extName = (url .getProtocol() == null ? "impl1" : url .getProtocol());或者: String extName = url .getMethodParameter(methodName, "loadbalance" , "random" );或者: String extName = url .getParameter("key1" , url .getParameter("key2" , "impl1" ));
生成扩展对象、调用目标方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private static final String CODE_EXTENSION_ASSIGNMENT = "%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);\n" ;private String generateMethodContent(Method method ) {... else { code.append(String . format(CODE_EXTENSION_ASSIGNMENT, type .getName() , ExtensionLoader .class .getSimpleName() , type .getName() )); code.append(generateReturnAndInvocation(method ) ); } return code.to String() ; } private String generateReturnAndInvocation(Method method ) { String returnStatement = method .getReturnType() .equals(void.class ) ? "" : "return " ; String args = IntStream . range(0 , method .getParameters() .length) .mapToObj(i -> String.format (CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i ) ) .collect(Collectors . joining(", " )); return returnStatement + String . format("extension.%s(%s);\n" , method .getName() , args); }
到这里,生成代理类代码的逻辑都在上述过程中,后续就是dubbo调用Compiler生成class,然后使用了。SPI到本文,源码讲解就结束了,后面就是我们自己手动实现SPI了。